部落格的 Blueprint 和會員驗證時候的做法一樣
部落格頁面應該列出所有文章,允許已登入的會員建立新文章,並允許作者修改和刪除文章
定義 blueprint 並且註冊到 application factory 中
flaskr/blog.py
from flask import (
Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort
from flaskr.auth import login_required
from flaskr.db import get_db
bp = Blueprint('blog', __name__)
和認證一樣使用 app.register_blueprint() 在工廠中導入和註冊 blueprint
將新的程式放在工廠函數的 return 之前
flaskr/init.py
def create_app():
app = ...
# existing code omitted
from . import blog
app.register_blueprint(blog.bp)
app.add_url_rule('/', endpoint='index')
return app
與驗證的 blueprint 不同,部落格 blueprint 沒有url_prefix
的前綴
所以index
view 會用於/
,create
會用於/create
,以此類推
部落格是 Flaskr 的主要功能,因此把部落格作為首頁是合理的!
但是,下文的index
view 的 endpoint 會被定義為blog.index
一些驗證的驗證 view 會重新導向到叫做index
的 endpoint
這邊使用 app.add_url_rule() 指定 路徑/
的 endpoint 名稱為'index'
這樣url_for('index')
或url_for('blog.index')
都會產生同樣指向/
路徑的網址
在其他應用程式中,可能會在工廠中給部落格的 blueprint 一個url_prefix
並定義一個獨立的index
view
類似之前做過的hello
view。在這種情況下index
和blog.index
的 endpoint 和網址會有所不同
首頁會從新到舊顯示所有文章,使用JOIN
來取得文章作者的資料
flaskr/blog.py
@bp.route('/')
def index():
db = get_db()
posts = db.execute(
'SELECT p.id, title, body, created, author_id, username'
' FROM post p JOIN user u ON p.author_id = u.id'
' ORDER BY created DESC'
).fetchall()
return render_template('blog/index.html', posts=posts)
flaskr/templates/blog/index.html
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Posts{% endblock %}</h1>
{% if g.user %}
<a class="action" href="{{ url_for('blog.create') }}">New</a>
{% endif %}
{% endblock %}
{% block content %}
{% for post in posts %}
<article class="post">
<header>
<div>
<h1>{{ post['title'] }}</h1>
<div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
</div>
{% if g.user['id'] == post['author_id'] %}
<a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
{% endif %}
</header>
<p class="body">{{ post['body'] }}</p>
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{% endblock %}
當使用者登入後header
區塊增加了一個指向create
view 的網址
當使用者是文章作者時,可以看到一個「Edit」網址,指向update
view
loop.last
是一個在 Jinja for 迴圈內部可用的特殊變數
用於在每個文章後面顯示一條線來分隔,最後一篇文章除外
create 的 view 和 register view 原理相同,負責顯示表單或是送出內容
並將通過驗證的資料已加入資料庫,或者顯示錯誤訊息
之前寫在auth.py
的login_required
裝飾器在這邊就用上了!
使用者必須登入以後才能訪問這些 view,否則會被跳轉到登入頁面
flaskr/blog.py
@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
if request.method == 'POST':
title = request.form['title']
body = request.form['body']
error = None
if not title:
error = 'Title is required.'
if error is not None:
flash(error)
else:
db = get_db()
db.execute(
'INSERT INTO post (title, body, author_id)'
' VALUES (?, ?, ?)',
(title, body, g.user['id'])
)
db.commit()
return redirect(url_for('blog.index'))
return render_template('blog/create.html')
flaskr/templates/blog/create.html
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}New Post{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="title">Title</label>
<input name="title" id="title" value="{{ request.form['title'] }}" required>
<label for="body">Body</label>
<textarea name="body" id="body">{{ request.form['body'] }}</textarea>
<input type="submit" value="Save">
</form>
{% endblock %}
update
和delete
view 都需要通過id
來取得post
,並且檢查作者與登入的使用者是否一致?
因為這部分是重複使用的,可以寫一個函數來取得post
,並且在不同的 view 中呼叫來使用
取得文章的函數:
flaskr/blog.py
def get_post(id, check_author=True):
post = get_db().execute(
'SELECT p.id, title, body, created, author_id, username'
' FROM post p JOIN user u ON p.author_id = u.id'
' WHERE p.id = ?',
(id,)
).fetchone()
if post is None:
abort(404, f"Post id {id} doesn't exist.")
if check_author and post['author_id'] != g.user['id']:
abort(403)
return post
abort() 會引發一個特殊的異常,回傳一個 HTTP 狀態碼
它有一個用於顯示出錯資訊的選填參數,沒傳入該參數則回傳預設錯誤訊息
404
代表頁面不存在,403
代表禁止訪問
在401
未授權的情況下,我們跳轉到登入頁而不是直接回傳這個狀態碼
使用check_author
參數的作用是用於不檢查作者的情況下獲取一個 post
這主要用於顯示獨立的文章頁面的情況,因為這時使用者是誰都沒有關係
更新文章:
flaskr/blog.py
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
post = get_post(id)
if request.method == 'POST':
title = request.form['title']
body = request.form['body']
error = None
if not title:
error = 'Title is required.'
if error is not None:
flash(error)
else:
db = get_db()
db.execute(
'UPDATE post SET title = ?, body = ?'
' WHERE id = ?',
(title, body, id)
)
db.commit()
return redirect(url_for('blog.index'))
return render_template('blog/update.html', post=post)
和所有以前的 view 不同,update
函數有一個id
參數
該參數對應路由中的<int:id>
,一個真正的 URL 類似/1/update
Flask 會捕捉到 URL 中的1
,確保格式是int
,並將其作為id
參數傳遞給 view
如果只寫了<id>
而沒有指定int:
的話就會用字串的方式傳遞
要產生一個更新頁面的 URL,需要將 id 參數加入 url_for()
例如:url_for('blog.update', id=post['id'])
,前面的index.html
檔案中也是
create
和update
的 view 看上去是相似的,主要的不同之處在於update
view 使用了post
物件
和一個UPDATE
query 而不是INSERT
query
作為一個明智的重構者,可以使用一個 view 和一個模板來同時完成這兩項工作
但是作為一個初學者,把它們分別處理會更容易理解一些
flaskr/templates/blog/update.html
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="title">Title</label>
<input name="title" id="title"
value="{{ request.form['title'] or post['title'] }}" required>
<label for="body">Body</label>
<textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
<input type="submit" value="Save">
</form>
<hr>
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
<input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
</form>
{% endblock %}
這個模板有兩個 form,第一個提交已編輯過的數據給當前頁面(/<id>/update
)
另一個 form 只包含一個按鈕,它指定一個action
屬性,指向刪除 view
這個按鈕使用了一些 JavaScript 用來在送出前顯示一個確認對話框
參數{{ request.form['title'] or post['title'] }}
用於選擇在表單顯示什麼資料
當表單還未送出時,顯示原本的 post 資料
但是,如果提交了無效的資料,你希望顯示錯誤以便於使用者修改時
就顯示request.form
中的資料,request 又是一個自動在模板中可用的變數!
刪除視圖沒有自己的模板,刪除按鈕已包含於update.html
之中
該按鈕指向/<id>/delete
URL
既然沒有模板,該 view 只處理 POST 方法並重新導向到index
view
flaskr/blog.py
@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
get_post(id)
db = get_db()
db.execute('DELETE FROM post WHERE id = ?', (id,))
db.commit()
return redirect(url_for('blog.index'))
接著就是測試功能的時間!試試看功能是否可以正常使用吧